#pragma once

#include <windows.h>

namespace AsyncLib
{

// Cannot be used for multithreading
class CAsyncFile
{
public:
	CAsyncFile() : m_hFile(INVALID_HANDLE_VALUE), m_nOffset(0)
	{
	}

	CAsyncFile(HANDLE hFile) : m_hFile(hFile), m_nOffset(0), m_pFileTask(GetAsyncLib().CreateFileTask(hFile))
	{
	}

	~CAsyncFile()
	{
		if (m_pFileTask)
			m_pFileTask->Close();

		if (m_hFile != INVALID_HANDLE_VALUE)
			CloseHandle(m_hFile);
	}

	CAsyncFile(const CAsyncFile&) = delete;
	CAsyncFile& operator=(const CAsyncFile&) = delete;

	[[nodiscard]] static CCoTask<DWORD> ReadAllAsync(LPCWSTR lpFileName, std::string& strData)
	{
		CAsyncFile aFile;
		DWORD dwError = co_await aFile.CreateAsync(lpFileName);

		if (dwError)
			co_return dwError;

		UINT nFileSize = (UINT)aFile.GetFileSize();
		if (nFileSize == INVALID_FILE_SIZE)
			co_return GetLastError();

		strData.resize(nFileSize);
		UINT nBytesRead = 0;
		dwError = co_await aFile.ReadAsync((char *)strData.c_str(), nFileSize, &nBytesRead);
		assert(nBytesRead == nFileSize || dwError);
		co_return dwError;
	}

	[[nodiscard]] static CCoTask<DWORD> WriteAllAsync(LPCWSTR lpFileName, LPCVOID lpBuffer, UINT nBytesToWrite)
	{
		CAsyncFile aFile;
		DWORD dwError = co_await aFile.CreateAsync(lpFileName, GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS);

		if (dwError)
			co_return dwError;

		UINT nBytesWritten = 0;
		dwError = co_await aFile.WriteAsync(lpBuffer, nBytesToWrite, &nBytesWritten);
		assert(nBytesWritten == nBytesToWrite || dwError);
		co_return dwError;
	}

	[[nodiscard]] CCoTask<DWORD> CreateAsync(LPCWSTR lpFileName, DWORD dwDesiredAccess = GENERIC_READ, DWORD dwShareMode = FILE_SHARE_READ, DWORD dwCreationDisposition = OPEN_EXISTING)
	{
		CUniqueLock lock(m_mutex);
		co_await lock.LockAsync();
		assert(!m_pFileTask);

		if (m_pFileTask)
			co_return ERROR_ALREADY_EXISTS;

		auto [hFile, dwError] = co_await PostTask([=]
		{
			HANDLE hFile = CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, NULL, dwCreationDisposition, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
			return std::pair(hFile, hFile == INVALID_HANDLE_VALUE ? GetLastError() : 0);
		});

		if (dwError)
			co_return dwError;

		m_hFile = hFile;
		m_pFileTask = GetAsyncLib().CreateFileTask(hFile);
		co_return m_pFileTask ? 0 : ERROR_OPERATION_ABORTED;
	}

	[[nodiscard]] CCoTask<DWORD> ReadAsync(LPVOID lpBuffer, UINT nBytesToRead, UINT *lpBytesRead)
	{
		if (lpBytesRead)
			*lpBytesRead = 0;

		CUniqueLock lock(m_mutex);
		co_await lock.LockAsync();
		assert(m_pFileTask);

		if (!m_pFileTask)
			co_return ERROR_INVALID_HANDLE;

		auto [nBytesRead, dwError] = co_await WrapFnAsAsync<std::pair<UINT, DWORD>>(&IFileTask::Read, m_pFileTask, lpBuffer, m_nOffset, nBytesToRead);
		assert(nBytesRead || dwError);
		m_nOffset += nBytesRead;

		if (lpBytesRead)
			*lpBytesRead = nBytesRead;

		co_return dwError;
	}

	[[nodiscard]] CCoTask<DWORD> WriteAsync(LPCVOID lpBuffer, UINT nBytesToWrite, UINT *lpBytesWritten)
	{
		if (lpBytesWritten)
			*lpBytesWritten = 0;

		CUniqueLock lock(m_mutex);
		co_await lock.LockAsync();
		assert(m_pFileTask);

		if (!m_pFileTask)
			co_return ERROR_INVALID_HANDLE;

		auto [nBytesWritten, dwError] = co_await WrapFnAsAsync<std::pair<UINT, DWORD>>(&IFileTask::Write, m_pFileTask, lpBuffer, m_nOffset, nBytesToWrite);
		assert(nBytesWritten == nBytesToWrite || dwError);
		m_nOffset += nBytesWritten;

		if (lpBytesWritten)
			*lpBytesWritten = nBytesWritten;

		co_return dwError;
	}

	[[nodiscard]] INT64 GetFileSize()
	{
		CSharedLock lock(m_mutex);
		bool bLocked = lock.TryLock();
		assert(bLocked && m_hFile != INVALID_HANDLE_VALUE);

		if (!bLocked)
		{
			SetLastError(ERROR_IO_INCOMPLETE);
			return INVALID_FILE_SIZE;
		}

		LARGE_INTEGER liFileSize{};
		return ::GetFileSizeEx(m_hFile, &liFileSize) ? liFileSize.QuadPart : INVALID_FILE_SIZE;
	}

	INT64 SetFilePointer(INT64 nDistanceToMove, DWORD dwMoveMethod)
	{
		CUniqueLock lock(m_mutex);
		bool bLocked = lock.TryLock();
		assert(bLocked && m_hFile != INVALID_HANDLE_VALUE);

		if (!bLocked)
		{
			SetLastError(ERROR_IO_INCOMPLETE);
			return INVALID_SET_FILE_POINTER;
		}

		if (dwMoveMethod == FILE_CURRENT)
		{
			dwMoveMethod = FILE_BEGIN;
			nDistanceToMove += m_nOffset;
		}

		LARGE_INTEGER liPosition{};
		liPosition.QuadPart = nDistanceToMove;
		return ::SetFilePointerEx(m_hFile, liPosition, &liPosition, dwMoveMethod) ? (m_nOffset = liPosition.QuadPart) : INVALID_SET_FILE_POINTER;
	}

	BOOL SetEndOfFile()
	{
		CUniqueLock lock(m_mutex);
		bool bLocked = lock.TryLock();
		assert(bLocked && m_hFile != INVALID_HANDLE_VALUE);

		if (!bLocked)
		{
			SetLastError(ERROR_IO_INCOMPLETE);
			return FALSE;
		}

		return ::SetEndOfFile(m_hFile);
	}

private:
	HANDLE m_hFile;
	INT64  m_nOffset;
	CAsyncMutex m_mutex;
	std::shared_ptr<IFileTask> m_pFileTask;
};

} // namespace AsyncLib

namespace AL
{

using AsyncLib::CAsyncFile;

}
